/**
 * File:   DemoNode.java
 * Author: Gvennet
 */
package dht.async;

import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.DelayQueue;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/** todo write javadoc */
public class DemoNode implements Node
{

    private int identifierLength = 20;

    private Identifier id;

    private RoutingTable rt;

    private Queue tq;

    private Communicator communicator;

    private List<Node> buffer;

    private long timeout;

    private Lock bufferLock;


    protected DemoNode(Communicator communicator)
    {
        this.communicator = communicator;
        id = ByteArrayIdentifier.randomIdentifier(identifierLength);
        rt = new RoutingTable(id);
        tq = new DelayQueue();
        buffer = new LinkedList<Node>();
        timeout = 5;
        bufferLock = new ReentrantLock();
    }

    @Override
    public long getDelay(TimeUnit timeUnit)
    {
        return timeout;
    }

    @Override
    public int compareTo(Delayed delayed)
    {
        return 0; // todo
    }

    @Override
    public Identifier getIdentifier()
    {
        return id;
    }

    @Override
    public RoutingTable getRoutingTable()
    {
        return rt;
    }


    @Override
    public void outboundPingRequest(Node receiver)
    {
        tq.offer(this);
        Message m = new Message(0, receiver, this, null);
        communicator.sendMessage(m);
    }

    @Override
    public void inboundPingRequest(Message message)
    {
        outboundPingResponse(message.getSender());
        updateRoutingTable(message.getSender());
    }

    @Override
    public void outboundPingResponse(Node receiver)
    {
        Message m = new Message(1, receiver, this, null);
        communicator.sendMessage(m);
    }

    @Override
    public void inboundPingResponse(Message message)
    {
        tq.remove(message.getSender());
        updateRoutingTable(message.getSender());
    }

    @Override
    public void outboundFindNodeRequest(Node receiver, Identifier target)
    {
        tq.offer(this);
        Message m = new Message(2, receiver, this, target);
        communicator.sendMessage(m);
    }

    @Override
    public void inboundFindNodeRequest(Message message)
    {
        List<Node> nodes = rt.getBucket(((Node)message.getData()).getIdentifier());
        System.out.println(nodes.size());
        outboundFindNodeResponse(message.getSender(), nodes);
        updateRoutingTable(message.getSender());
    }

    @Override
    public void outboundFindNodeResponse(Node receiver, List<Node> nodes)
    {
        Message m = new Message(3, receiver, this, nodes);
        communicator.sendMessage(m);
    }

    @Override
    public void inboundFindNodeResponse(Message message)
    {
        tq.remove(message.getSender());
        List<Node> nodes = (List<Node>) message.getData();
        for (Node node: nodes)
        {
            System.out.println(node.getIdentifier());
        }
        //todo во всех подобных местах сделать проверку правильности "кастования"
        // todo а не поменять ли тип функции на List<Node>?
    }

    @Override
    public void timeout(Node node)
    {
        rt.remove(node);
        bufferLock.lock();
        try{
        for (int i = buffer.size(); i > 0; i--)
        {
            if (rt.havePlace(buffer.get(i).getIdentifier()))
            {
                rt.add(buffer.get(i));
            }
        }
        }
        finally
        {
            bufferLock.unlock();
        }
    }

    @Override
    public void receive(Message message)
    {
        //System.out.println("type: "+message.getType()+" sender = "+message.getSender().getIdentifier()+", receiver = "+message.getReceiver().getIdentifier());
        switch (message.getType())
        {
            case 0:
            {
                inboundPingRequest(message);
                break;
            }
            case 1:
            {
                inboundPingResponse(message);
                break;
            }
            case 2:
            {
                inboundFindNodeRequest(message);
                break;
            }
            case 3:
            {
                inboundFindNodeResponse(message);
                break;
            }
            default:
                break;
        }
    }

    @Override
    public void updateRoutingTable(Node node)
    {
        //System.out.println(this.getIdentifier()+" "+rt.getAllNodes());
        Identifier id = node.getIdentifier();
        if (rt.contains(id))
        {
            rt.makeFirst(id);
            //System.out.println("makeFirst");
        }
        else
        {
            if (rt.havePlace(id))
            {
                rt.add(node);
                //  System.out.println("Add");
            }
            else
            {
                //System.out.println("Send ping");
                bufferLock.lock();
                try{
                if (buffer.size() > 20)
                {
                    buffer.remove(20);
                }
                buffer.add(node);
                }
                finally
                {
                    bufferLock.unlock();
                }
                int i = rt.findBucket(id);
                rt.lockBucket(i);
                try
                {
                    for (Node aNode : rt.getBucket(id))
                    {
                        outboundPingRequest(aNode);
                    }
                }
                finally
                {
                    rt.unlockBucket(i);
                }
            }
        }
    }

}
